Skip to content

Add Lambda Durable Functions order processing pattern with CDK#2926

Open
smyvens wants to merge 6 commits intoaws-samples:mainfrom
smyvens:main
Open

Add Lambda Durable Functions order processing pattern with CDK#2926
smyvens wants to merge 6 commits intoaws-samples:mainfrom
smyvens:main

Conversation

@smyvens
Copy link

@smyvens smyvens commented Feb 5, 2026

Pattern: lambda-durable-function-chaining-cdk

This pattern demonstrates an e-commerce order processing workflow using AWS Lambda Durable Functions with function chaining, deployed via AWS CDK.

How it works

Processes orders through a 4-step workflow: validation → payment → inventory → fulfillment. The orchestrator function uses durable execution to checkpoint each step, so if something fails, it picks up where it left off instead of starting over.

The payment function fetches pricing from DynamoDB, inventory gets decremented when allocated, and if fulfillment fails, there's compensation logic to put the inventory back.

Architecture

  • 1 Durable Lambda function (orchestrator) with 1-hour execution timeout
  • 3 Regular Lambda functions (workers)
  • 1 DynamoDB table for product catalog
  • Asynchronous invocation pattern for long-running workflows

@bfreiberg bfreiberg added the durable functions Pattern for AWS Lambda durable functions label Feb 10, 2026
@bfreiberg
Copy link
Contributor

Review findings

No pattern metadata JSON file was found in the pattern root directory.

Please see https://github.com/aws-samples/serverless-patterns/tree/main/_pattern-model for required files

VS Code Configuration Directory Committed

The .vscode/ directory contains IDE-specific settings that are developer-specific and should not be committed to a community pattern repository.

Recommended Fix:

rm -rf .vscode/
echo ".vscode/" >> .gitignore

Missing Steps in README

Incorrect Service Name

  • It's called Lambda durable functions not Lambda Durable Functions
  • The README title uses "AWS Lambda Durable Functions" but the body text frequently refers to "Lambda" without the "AWS" prefix on first reference in several sections. Similarly for DynamoDB where the correct first-reference form is "Amazon DynamoDB". Also Amazon CloudWatch.

Folder Casing Mismatch Between CDK Stack and File System

The CDK stack references path.join(__dirname, "functions", "allocateInventory", "index.ts") and path.join(__dirname, "functions", "fulfillOrder", "index.ts") but the actual directories are AllocateInventory and FulfillOrder (PascalCase). On case-sensitive file systems (Linux, which Lambda uses), this mismatch will cause deployment failures because the files won't be found.

DynamoDB Table Missing RemovalPolicy for Demo Pattern

The ProductCatalogTable does not set a removalPolicy. By default, CDK retains DynamoDB tables on stack deletion, which means running cdk destroy will leave an orphaned table incurring storage costs. For a demo/example pattern, RemovalPolicy.DESTROY is more appropriate.

Non-Atomic Inventory Check and Update (Race Condition)

  • File: lib/functions/AllocateInventory/index.ts (lines 55-85)
  • Issue: The inventory allocation performs a GetItemCommand to check stock level, then a separate UpdateItemCommand to decrement it. Between these two operations, another concurrent request could read the same stock level and both would succeed, over-allocating inventory. A conditional update with ConditionExpression would make this atomic.
  • Current Code/Configuration:
    const stockLevel = parseInt(result.Item.stockLevel.N!);
    if (stockLevel < quantity) { return { status: "failed" }; }
    await dynamoClient.send(new UpdateItemCommand({
      UpdateExpression: "SET stockLevel = :newStock",
      ExpressionAttributeValues: { ":newStock": { N: String(stockLevel - quantity) } },
    }));
  • Recommended Fix:
    // Use a single conditional UpdateItem instead of GetItem + UpdateItem
    try {
      await dynamoClient.send(new UpdateItemCommand({
        TableName: process.env.PRODUCT_CATALOG_TABLE,
        Key: { productId: { S: productId } },
        UpdateExpression: "SET stockLevel = stockLevel - :quantity",
        ConditionExpression: "stockLevel >= :quantity",
        ExpressionAttributeValues: { ":quantity": { N: String(quantity) } },
      }));
    } catch (error) {
      if (error.name === "ConditionalCheckFailedException") {
        return { orderId, status: "failed", reason: "insufficient_inventory", productId };
      }
      throw error;
    }

Missing CloudWatch Log Retention Policy

  • File: lib/lambda-durable-function-chaining-cdk-stack.ts (global)
  • Issue: No explicit CloudWatch Log Group retention is configured for any of the four Lambda functions. Logs will be retained indefinitely, accumulating storage costs over time.
  • Current Code/Configuration:
    N/A — No log retention configuration present.
    
  • Recommended Fix:
    // Add logRetention to each NodejsFunction
    const validateOrderFunction = new nodejs.NodejsFunction(this, "ValidateOrderFunction", {
      // ... existing config
      logRetention: logs.RetentionDays.ONE_WEEK,
    });

Non-Deterministic Date Calls Outside Steps

  • File: lib/functions/validateOrder/index.ts (lines 55, 63, 72, 80, 88, 130)
  • Issue: Multiple new Date().toISOString() calls occur outside context.step() blocks in the durable handler's return statements. On replay, these produce different timestamps each time the handler re-executes, causing the final return value to contain inconsistent data across replays.
  • Current Code:
    // These are outside steps — different on every replay!
    return {
      orderId,
      status: "rejected",
      reason: "validation_failed",
      errors: validation.errors,
      timestamp: new Date().toISOString(), // Non-deterministic!
    };
    // ... and similar in other return blocks and completedAt field
  • Recommended Fix:
    // Generate timestamps inside steps
    const timestamp = await context.step('get-timestamp', async () => new Date().toISOString());
    // Use the checkpointed timestamp in return values
    return {
      orderId,
      status: "rejected",
      reason: "validation_failed",
      errors: validation.errors,
      timestamp,
    };

Nested Durable Step Inside fulfill-order Step

  • File: lib/functions/validateOrder/index.ts (lines 96-112)
  • Issue: Inside the fulfill-order step callback, there is a nested context.step("restore-inventory", ...) call. Nesting durable operations inside steps is not allowed by the SDK and produces runtime errors or undefined behavior. The restore-inventory logic should be moved outside the fulfill-order step or the entire block should use context.runInChildContext().
  • Current Code:
    const fulfillment = await context.step("fulfill-order", async () => {
      const response = await lambdaClient.send(new InvokeCommand({ /* ... */ }));
      const payload = JSON.parse(new TextDecoder().decode(response.Payload));
      if (payload.status === "failed") {
        await context.step("restore-inventory", async () => {  // NESTED! Not allowed
          await lambdaClient.send(new InvokeCommand({ /* ... */ }));
        });
        return { /* ... */ };
      }
      return payload;
    });

Manual Lambda Invoke Instead of context.invoke()

  • File: lib/functions/validateOrder/index.ts (lines 59-70, 77-86, 91-100)
  • Issue: The orchestrator uses lambdaClient.send(new InvokeCommand(...)) inside context.step() blocks to invoke worker functions. The durable SDK provides context.invoke() for Lambda-to-Lambda calls with automatic checkpointing and retry handling. Using manual invocation bypasses the SDK's built-in checkpoint mechanism for invocations.
  • Current Code:
    const authorization = await context.step("authorize-payment", async () => {
      const response = await lambdaClient.send(
        new InvokeCommand({
          FunctionName: process.env.AUTHORIZE_PAYMENT_FUNCTION,
          Payload: JSON.stringify({ orderId, customerId: event.customerId, items, paymentMethod }),
        }),
      );
      const payload = JSON.parse(new TextDecoder().decode(response.Payload));
      return payload;
    });
  • Recommended Fix:
    const authorization = await context.invoke(process.env.AUTHORIZE_PAYMENT_FUNCTION, {
      orderId,
      customerId: event.customerId,
      items,
      paymentMethod,
    });

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

durable functions Pattern for AWS Lambda durable functions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants